/**
 * Copyright 2008 Mathias Doenitz, http://lis.to/
 *
 * This file is part of the lis.to java desktop client. The lis.to java desktop client is free software: you can
 * redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * The lis.to java desktop client is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with the lis.to java desktop client.
 * If not, see http://www.gnu.org/licenses/
 */

package listo.utils.reflection;

import com.google.common.collect.Lists;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import listo.utils.MiscUtils;
import listo.utils.caching.Cache;
import listo.utils.logging.Log;
import org.apache.commons.lang.StringUtils;

import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

/**
 * Provides general, not object specific reflection functionality.
 */
@Singleton
class ReflectorImpl implements Reflector {

    private final Log log;
    private final Cache<String, List<Class>> packageClassesCache;
    private final Cache<Integer, Class> handlerClassCache;

    @Inject
    public ReflectorImpl(Cache<String, List<Class>> packageClassesCache, Cache<Integer, Class> handlerClassCache,
                         Log log) {
        this.packageClassesCache = packageClassesCache;
        this.handlerClassCache = handlerClassCache;
        this.log = log;
    }

    /**
     * Creates a ReflectorWrapper for the given object.
     *
     * @param obj the object
     * @return the wrapper
     */
    public ReflectorWrapper f(Object obj) {
        return new ReflectorWrapperImpl(obj, this);
    }

    /**
     * Creates a ReflectorWrapper for the given class.
     *
     * @param clazz the class
     * @return the wrapper
     */
    public ReflectorWrapper f(Class clazz) {
        return new ReflectorWrapperImpl(clazz, this);
    }

    /**
     * Gets the classes contained in the packages with the given names.
     *
     * @param packageNames the packages to search through
     * @return the list of classes found in the packages
     */
    @SuppressWarnings("unchecked")
    public List<Class> getClassesOfPackages(String... packageNames) {
        List<Class> list = new LinkedList<Class>();

        for (String packageName : packageNames) {
            List<Class> packageClasses = packageClassesCache.get(packageName);
            if (packageClasses == null) {
                packageClasses = getClassesOfPackage(packageName);
                packageClassesCache.put(packageName, packageClasses);
            }
            list.addAll(packageClasses);
        }

        return list;
    }

    /**
     * Creates a list of classes found on the file system for the given package name.
     * using the package name as key.
     *
     * @param packageName the package name
     * @return the classes found
     */
    private List<Class> getClassesOfPackage(String packageName) {
        String classPath = System.getProperty("java.class.path");
        char fileSep = System.getProperty("file.separator").charAt(0);
        char pathSep = System.getProperty("path.separator").charAt(0);

        //log.debug("Getting classes of package %1$s from class path %2$s", packageName, classPath);

        List<Class> classes = Lists.newArrayList();
        String[] pathComponents = StringUtils.split(classPath, pathSep);
        for (String pathComponent : pathComponents) {
            try {
                File file = new File(pathComponent);
                if (file.isDirectory()) {
                    crawlDirectory(file, classes, packageName, packageName.replace('.', fileSep));
                    continue;
                }

                String pathCompLow = pathComponent.toLowerCase();
                if (file.isFile() && (pathCompLow.endsWith(".jar") || pathCompLow.endsWith(".zip") || pathCompLow
                        .endsWith(".exe"))) {
                    crawlZipFile(file, classes, packageName, packageName.replace('.', '/'));
                    continue;
                }

                log.warn("Unknown class path entry %1$s", pathComponent);
            }
            catch (IOException e) {
                log.warn("Error accessing class path component %1$s:\n%2$s", pathComponent, e.getMessage());
            }
        }
        return classes;
    }

    private void crawlDirectory(File directory, List<Class> classes, String packageName, String resourceName)
            throws IOException {
        //log.debug("Crawling directory %1$s", directory);

        boolean collect = directory.getCanonicalPath().endsWith(resourceName);
        for (File file : directory.listFiles()) {
            if (collect && file.isFile()) {
                collectClass(classes, packageName, file.getName());
            } else if (file.isDirectory()) {
                crawlDirectory(file, classes, packageName, resourceName);
            }
        }
    }

    private void crawlZipFile(File file, List<Class> classes, String packageName, String resourceName)
            throws IOException {
        //log.debug("Crawling zip/jar file %1$s", file);

        ZipFile zipFile = new ZipFile(file);
        int resourceNameLen = resourceName.length();

        Enumeration<? extends ZipEntry> entryEnum = zipFile.entries();
        while (entryEnum.hasMoreElements()) {
            String entry = entryEnum.nextElement().getName();
            if (entry.startsWith(resourceName)) {
                entry = entry.substring(resourceNameLen + 1);
                if (entry.indexOf('/') == -1) collectClass(classes, packageName, entry);
            }
        }
    }

    private void collectClass(List<Class> classes, String packageName, String filename) {
        if (!filename.endsWith(".class")) return;

        // removes the .class extension and build a proper class name
        String className = packageName + '.' + filename.substring(0, filename.length() - 6);
        try {
            //log.debug("Adding class %1$s", className);
            classes.add(Class.forName(className));
        }
        catch (ClassNotFoundException e) {
            log.error("Class '%1$s' not found", className);
        }
    }

    /**
     * Finds the best matching handler class for a given object class and naming pattern.
     * If no match is found an exception is being thrown.
     *
     * @param objectClass the class of the object to base the search on
     * @param namePattern the name pattern for the search
     * @return the best matching handler class
     */
    protected Class getHandlerClass(Class objectClass, String namePattern) {
        int cacheKey = namePattern.hashCode() ^ objectClass.hashCode();
        Class handlerClass = handlerClassCache.get(cacheKey);
        if (handlerClass != null) return handlerClass; // cache hit

        // construct a list of all matching types in descending order of matching quality
        List<Class> matchingClasses = new LinkedList<Class>();

        // first: add all parent classes
        Class c = objectClass;
        while (c != null) {
            matchingClasses.add(c);
            c = c.getSuperclass();
        }

        // then all interfaces
        matchingClasses.addAll(Arrays.asList(objectClass.getInterfaces()));

        // search for existing handler classes and return first class found
        for (Class matchingClass : matchingClasses) {
            String shortName = MiscUtils.getShortClassName(matchingClass);
            String handlerClassName = namePattern.replace("%CLASS%", shortName);
            try {
                handlerClass = Class.forName(handlerClassName);

                // ok, class found, so add it to the cache and return
                handlerClassCache.put(cacheKey, handlerClass);
                return handlerClass;
            }
            catch (ClassNotFoundException e) {
                log.warn("Handler '%1$s' class not found", handlerClass);
            }
        }
        throw new RuntimeException("Handler class not found for type " + objectClass.getName());
    }

    /**
     * Returns the most specialized common base type of the given objects or null,
     * if the given array is empty or contains at least one null entry.
     *
     * @param objects the objects
     * @return the common base type or null
     */
    public Class getCommonType(Object[] objects) {
        Class commonBase = null;
        for (Object obj : objects) {
            if (obj == null) return null;

            if (commonBase == null) {
                commonBase = obj.getClass();
                continue;
            }

            ReflectorWrapper clazz = f(obj.getClass());
            while (!clazz.canBeCastTo(commonBase)) {
                commonBase = commonBase.getSuperclass();
            }
        }
        return commonBase;
    }

}
